/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *ㄔ
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mozilla.
 *
 * The Initial Developer of the Original Code is IBM Corporation.
 * Portions created by IBM Corporation are Copyright (C) 2004
 * IBM Corporation. All Rights Reserved.
 *
 * Contributor(s):
 *   Darin Fisher <darin@meer.net>
 *   Doron Rosenberg <doronr@us.ibm.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

//Modified by KangHao Lu (kennyluck) 
//<http://dig.csail.mit.edu/People/kennyluck#I> on 2009-11-30 

// Test protocol related
// use bbs:// instead of telnet:// becuase telnet:// and ssh:// have other
// predefined behavior(@maybe bbs: or bbs:/)
const kSCHEME              = "bbs";
const kPROTOCOL_NAME       = "BBS Protocol";
const kPROTOCOL_CONTRACTID = "@mozilla.org/network/protocol;1?name=" + kSCHEME;
const kPROTOCOL_CID        = Components.ID("327d99f0-dda8-11de-8a39-0800200c9a66");

// Mozilla defined
const kSTANDARDURL_CONTRACTID = "@mozilla.org/network/standard-url;1";
const kSIMPLEURI_CONTRACTID   = "@mozilla.org/network/simple-uri;1";
const kIOSERVICE_CONTRACTID   = "@mozilla.org/network/io-service;1";

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const nsISupports        = Components.interfaces.nsISupports;
const nsIIOService       = Components.interfaces.nsIIOService;
const nsIProtocolHandler = Components.interfaces.nsIProtocolHandler;
const nsIStandardURL     = Components.interfaces.nsIStandardURL;
const nsIURI             = Components.interfaces.nsIURI;

//This function is used to report the correct fileName and lineNumber
//associated with the exception. see also <https://wave.google.com/wave/?pli=1#restored:search:tag%253Aon-the-wave,restored:wave:googlewave.com!w%252BVOBYqc5xB.3> for usage
function handleException(e, fileName, lineDiff){
  if (!e.fileName || 
      e.fileName.indexOf("install_bbs_protocol.js") == -1) throw e; 
  //when either the error is a string (nothing more to report) or the error
  //comes from somthing that's not part of install_bbs_protocol.js
  // (=> not executed with eval()), don't do anything.

  var scriptError =  Cc["@mozilla.org/scripterror;1"].
                        createInstance(Ci.nsIScriptError);
  var consoleService = Cc["@mozilla.org/consoleservice;1"].
                        getService(Ci.nsIConsoleService);
  scriptError.init(e.message, fileName, "", e.lineNumber - lineDiff,
		   0, 0, "XPConnect JavaScript");
  consoleService.logMessage(scriptError);
  throw "(Please ignore) This is a replication of the above error, for the purpose of blocking the execution";
}

//load JavaScript subscript in UTF8 into this execution context
//We used to use mozIJSSubScriptLoader, similar to what's done in
// components/knowledge_base.js, for importing 
//chrome/content/BBSConnection.js and PTTParser.js. But the default 
//encoding for the service is the undesirable ISO-8859-1, so is that
//of Components.utils.import. So a new method is created.
//see also <https://wave.google.com/wave/?pli=1#restored:search:tag%253Aon-the-wave,restored:wave:googlewave.com!w%252BVOBYqc5xB.3>
function loadSubScriptInUTF8(aURIString){
  var ios = Cc["@mozilla.org/network/io-service;1"].
            getService(Ci.nsIIOService);
  var channel = ios.newChannel(aURIString, null, null);
  var input = channel.open();
  var count = channel.contentLength;
  var scriptableInput = Cc["@mozilla.org/scriptableinputstream;1"].
                        createInstance(Ci.nsIScriptableInputStream);
  scriptableInput.init(input);
  var encodedScript = scriptableInput.read(count);
  if (encodedScript.length !== count) 
    throw "loadSubScriptInUTF8: content lengths don't match";
  
  //decode into Unicode string
  var decoder = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                createInstance(Ci.nsIScriptableUnicodeConverter);
  decoder.charset = 'utf-8';
  var script = decoder.ConvertToUnicode(encodedScript);

  //execute with this context, this makes sure variables defined above have
  //less priority
  try{
    with (this){
      eval(script);
    }
  } catch (e) { //Syntax Error, Eval Error(?)
    handleException(e, aURIString, 110);
  }  
  for (var i=0;i<EXPORTED_SYMBOLS.length;i++)
    this[EXPORTED_SYMBOLS[i]] = eval(EXPORTED_SYMBOLS[i]);
}

/*
  !!!IMPORTANT!!!
 
  Please do not modify anython above this point. If you really feel like
  doing so, please make sure thant LINEDIFF in PTTParser.js and 
  BBSConnection.js is correct. see also <https://wave.google.com/wave/?pli=1#restored:search:tag%253Aon-the-wave,restored:wave:googlewave.com!w%252BVOBYqc5xB.3>

*/

var gBBSProtocol = null; // the singleton for the following XPCOM service

function BBSProtocol() {
  this.wrappedJSObject = this;
  loadSubScriptInUTF8("chrome://on-the-wave/content/BBSConnection.js");
  loadSubScriptInUTF8("chrome://on-the-wave/content/PTTParser.js");
}

BBSProtocol.prototype = {
  QueryInterface: function(iid) {
    if (!iid.equals(nsIProtocolHandler) &&
        !iid.equals(nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  scheme: kSCHEME,
  defaultPort: 23,
  protocolFlags: nsIProtocolHandler.URI_NORELATIVE |
                 nsIProtocolHandler.URI_NOAUTH,
  
  allowPort: function(port, scheme) {
    return false;
  },

  newURI: function(spec, charset, baseURI) {
    /*
    var uri = Components.classes[kSIMPLEURI_CONTRACTID].createInstance(nsIURI);
    uri.spec = spec;
    return uri;
    */
    var cls = Components.classes[kSTANDARDURL_CONTRACTID];
    var url = cls.createInstance(nsIStandardURL);
    url.init(nsIStandardURL.URLTYPE_STANDARD, 23, spec, charset, baseURI);

    return url.QueryInterface(nsIURI);
  },

  newChannel: function(aURI) {
    return new BBSChannel(aURI);
  },

  //The rest of this protocol handler controls the BBS connection,
  //these methods call be called with:
  //Components.classes["@mozilla.org/network/protocol;1?name=bbs"].
  //getService().wrappedJSObject.methodName
  mRootConnectionDict: {}, // a string->connection dictionary,
                           // the name comes from nsFTPProtocolHandler,
                           // which uses similar strategy to deal with
                           // multiple connections
  lookUp: function lookUp(aURI){
    //lookUping includes dereferencing and parsing
    if (typeof aURI === "string")
      aURI = this.newURI(aURI, null, null); //convert to nsIURI
    var conn = (this.mRootConnectionDict[aURI.prePath] || 
		new BBSConnection(aURI.host));
    this.mRootConnectionDict[aURI.prePath] = conn;

    conn.lookUpLater(aURI.path);
    //conn.setCallback(aURI.path, function(){dump("callback fired\n")});
		      
  },

  setCallback: function addCallback(aURIString, aCallback){
    var aURI = this.newURI(aURIString, null, null);
    var conn = this.mRootConnectionDict[aURI.prePath];
    if (!conn) throw "bbshandler::addCallback: no BBSConnection for this URI.";
    
    conn.setCallback(aURI.path, aCallback);
  },
  tweakUriBack: function tweakUriBack(aURIString, aIsPost){
    gUriToTweak = aURIString;
    gIsPost = aIsPost;
  },
  
  // The rest is for setting up the component
  classDescription: kPROTOCOL_NAME,
  classID: kPROTOCOL_CID,
  contractID: kPROTOCOL_CONTRACTID,
  _xpcom_factory: {
    createInstance: function(aOuter, aIID) {
      if (aOuter != null)
        throw Components.results.NS_ERROR_NO_AGGREGATION;
      
      if (!gBBSProtocol)
        gBBSProtocol = new BBSProtocol();
      
      return gBBSProtocol.QueryInterface(aIID);
    }
  },
};

__defineGetter__("gIOService", function() {
  delete this.gIOService;
  return this.gIOService = Cc[kIOSERVICE_CONTRACTID]
                             .getService(Ci.nsIIOService);
});


__defineGetter__("gConverterToPostList", function() {
  delete this.gConverterToPostList;
  return this.gConverterToPostList = Cc["@mozilla.org/streamconv;1?from=application/onthewave.postlist&to=*/*"].getService().wrappedJSObject;
});

var gUriToTweak = "";
var gIsPost = null;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
//BBSChannel implements the nsIChannel for the BBS protocol, but it doesn't
//need to be registered as a XPCOM component. A XPCOM object implementing
//QueryInterface is enough
function BBSChannel(aURI){
  this.URI = aURI; //!!this.URI can only be set at the constructor...
  this.name = aURI.spec;
  var sampleChannel = gIOService.newChannel("chrome://on-the-wave/content/post_list.html", null, null);
  this.owner = sampleChannel.owner; //this grants the window full
                                    //XPConnect access
                                    //@ understand owner and remove 
                                    // sample channel
}

BBSChannel.prototype = {
  //intial values for attributes are based on nsBaseChannel
  //nsIRequest Attributes
  name: "", //name: -> URI
  status: Cr.NS_OK,
  loadGroup: null,  //(used to represent bundled resources)
  loadFlags: Ci.nsIRequest.LOAD_NORMAL,
  //nsIChannel Attributes
  originalURI: null, //@sure?
  URI: null, //@sure?
  owner: null, //@owner??
  notificationCallbacks: null, //@sure?
  securityInfo: null, //@securityInfo??
  contentType: "", //@sure?
  contentCharset: "", //@sure?
  contentLength: -1,
  //nsIRequest Methods
  isPending: function isPending(){
    return false;
  },
  cancel: function cancel(aStatus){
  },
  suspend: function suspend(){
  },
  resume: function resume(){
  },
  //nsIChannel Methods
  open: function open(){
    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  },
  asyncOpen: function asyncOpen(aListener, aContext){
    //(This is suppose to be an asynchronus call, but the following
    //code finishes the work at once. - NO LONGER TRUE) 
    //This code successed at 
    //5am 1-18-2010. It might be better to put a setTimeout {} around
    //the code to make it real asynchornous.

    //nsIChannel implementation is responsible for loadGroup settings
    if (this.loadGroup) //set by nsURILoader::OpenChannel
      this.loadGroup.addRequest(this, null);
    if (this.URI.spec === gUriToTweak){
      var origURI = this.URI.clone();
      origURI.scheme = "http";
      this.originalURI = origURI;
      gConverterToPostList.switchPresentation(gIsPost);
    } else {
      if (this.URI.path.split('/')[2])
        gConverterToPostList.switchPresentation(true);
      else
        gConverterToPostList.switchPresentation(false);
    /* @@@ //dump("newChannel called by:" + this.newChannel.caller + '\n'); //Firebug would call this function for some reason
       Don't push the URI onto the lookup queue if it is called by nsIIOService.newChannel (and in turn, called by, probably, Firebug's debugging service (not yet fully verified)).
       Other functions that call this:
       1. nsIWebNavigation::loadURI for normal URI looking up
       2. nsIHistory(?)::gotoIndex when reloading old session

       This following is a very very hacky of doing this. This has to to with the fact that a caching mechanism for BBS URIs are not yet designed. Also, it is not obvious that newChannel->lookUp, that is, nsBBSChannel::ayncOpen->lookUp makes more sense, but it's not possible unless nsBBSChannel is inplemented.
     
    if (this.newChannel.caller &&
	this.newChannel.caller.name != "newChannel")
      this.lookUp(aURI); */
      gBBSProtocol.lookUp(this.URI);
    }
    //content is ready from no where~~
    this.status = 0;
    this.contentType = 'application/onthewave.postlist';
    this.contentLength = 1; //@too hacky?
    var _this = this;
    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    var timerCallback = { notify: function(timer){
      aListener.onStartRequest(_this, aContext);
      aListener.onStopRequest(_this, aContext, 0);
      _this.originalURI = _this.URI;
      if (_this.loadGroup)
        _this.loadGroup.removeRequest(_this, null, Cr.NS_OK); //always fine
    }};
    timer.initWithCallback(timerCallback, 0, Ci.nsITimer.TYPE_ONE_SHOT);
    
  },
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel])
}

function NSGetModule(compMgr, fileSpec) 
  XPCOMUtils.generateModule([BBSProtocol])

